Skip to content

feat(ui): preserve email connector grant + scope consent under Path 2 (incl. Outlook CTA)#1775

Merged
itomek-amd merged 9 commits into
mainfrom
feat/ui-email-scope-consent-1770
Jun 19, 2026
Merged

feat(ui): preserve email connector grant + scope consent under Path 2 (incl. Outlook CTA)#1775
itomek-amd merged 9 commits into
mainfrom
feat/ui-email-scope-consent-1770

Conversation

@itomek

@itomek itomek commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Closes #1770. Part of the Path-2 email-in-Agent-UI epic #1767.

Stacked on #1774 (#1768, backend mount). Review after/with it; I'll retarget to main once #1774 merges.

Why this matters

Under Path 2 email is consumed as a backend REST surface, not an activated chat agent — but it still needs write-level OAuth scopes (gmail.modify+gmail.send+calendar.events / Mail.ReadWrite+Mail.Send+Calendars.ReadWrite). Before: consent was driven by email being a granted/activated chat agent, and the Connect CTA only offered Google (Outlook users had no path). If the user connected a mailbox without granting installed:email, write calls failed at call time. After: the Connectors panel surfaces email's requirement from the registry independent of chat activation, the CTA drives consent for both Google and Microsoft, and every insufficient/absent-scope state returns an actionable error + the Connect/Reconnect CTA — never a silent success or generic 500.

Evidence

22 Python regression tests — tests/test_ui_email_scope_consent.py (all pass) covering every AC:

$ python -m pytest tests/test_ui_email_scope_consent.py -q
22 passed

20 Vitest — EmailConnectCta.test.ts (all pass): provider-aware dual-button CTA (detectProvider, Microsoft auth-required paths). npm run build clean (2086 modules).

Real-world (in progress)

Live Gmail + Outlook consent (showing the requested email scopes) is being captured on macOS with the test accounts; cross-platform (Linux/Windows) follows. Posted as a follow-up.

Test plan

  • python -m pytest tests/test_ui_email_scope_consent.py -v → 22 passed
  • cd src/gaia/apps/webui && npm test -- EmailConnectCta && npm run build
  • Live: connect Google + Microsoft → consent shows the mail+calendar scopes; the 3 negative states show the actionable CTA

Tomasz Iniewicz added 6 commits June 19, 2026 14:21
Conditionally mounts the gaia_agent_email /v1/email/* router inside
create_app when the wheel is installed, mirroring the pattern already
used in gaia.api.openai_server. An absent wheel logs a clean info
message; a broken installed wheel propagates loudly per the
no-silent-fallback rule. Adds integration tests that cover the
mounted health/version/triage surface and the absent-wheel skip path.
…correct assertions

The #1768 integration test hung in the TestClient lifespan (the UI server's
startup tasks) and carried three wrong assertions that were never reached:
- use a non-lifespan TestClient (email routes are self-contained; the heavy
  startup can hang in a bare env, see #1297)
- health returns {status: ok}, not an 'ok' key
- absent-wheel paths fall through to the SPA catch-all (HTML 200), so assert
  the email router does not serve them rather than expecting 404
- patch find_spec targeted (no fragile module reload)
…tor (#1770)

Tests verify:
- installed:email REQUIRED_CONNECTORS exposes full Google+Microsoft scope sets
  independent of chat-agent activation (resolved from the class, not session)
- AGENT_NOT_GRANTED raises for both google/microsoft when no grant exists (403,
  not 500) — regression-guard against generic-500 regression
- CONNECTION_MISSING_SCOPES fires when OAuth token lacks calendar.events while
  mail scopes are present; triage/send mail-only scopes pass grant check
- resolve_send_backend raises HTTP 503 (actionable) when no mailbox connected
- Legacy builtin:email → installed:email grant migration is correct and idempotent
…or (#1770)

EmailConnectCta now detects which provider the connectors error references
(google / microsoft / both) and renders matching connect button(s) — resolving
the UI gap where Outlook users saw a Google-only prompt. ConnectorsSection
gains Microsoft Graph scope URI labels so the consent list is human-readable.
MessageBubble threads `content` to EmailConnectCta for provider detection.
@github-actions github-actions Bot added tests Test changes electron Electron app changes labels Jun 19, 2026
…nown connectorId

Narrow the connectorId override to the valid google/microsoft values and fall
back to content detection otherwise, removing the unchecked cast that could
silently hide both buttons (no-silent-fallback convention). Addresses a review
advisory.
@itomek

itomek commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator Author

Real-world (macOS) — live Google OAuth consent ✅

Verified on macOS via the Agent UI's in-app connector setup (no env vars — client creds entered in Settings → Connectors → Google, stored in the OS keyring):

  • Consent requested the full mail+calendar scope set. The Google OAuth URL carried gmail.modify + gmail.send + calendar.events (plus gmail.readonly/calendar.readonly/drive) — i.e. the write/send/calendar scopes the email agent requires.
  • Connected to the test account tomasz.iniewicz@gmail.com.
  • The Connectors panel surfaces the email requirement independent of chat activation — a "Email Triage" per-agent grant card appears with granular toggles: Organize emails (archive/label/trash) = gmail.modify, Send emails on your behalf = gmail.send, Create & respond to calendar events = calendar.events, View calendar events = calendar.readonly. (Calendar toggles default off — the live equivalent of negative-case (b).)

Cross-platform (Linux + Windows) OAuth runs next as a coordinated pass. The three negative-scope cases remain covered by the 22 Python tests.

@itomek itomek self-assigned this Jun 19, 2026
@itomek itomek marked this pull request as ready for review June 19, 2026 19:52
@itomek itomek requested a review from kovtcharov-amd as a code owner June 19, 2026 19:52
@github-actions

Copy link
Copy Markdown
Contributor

Summary

Clean, well-scoped frontend change that makes the email Connect CTA provider-aware (Google and Microsoft) and adds a thorough Python regression suite for the email connector's grant/scope consent under Path 2. I traced the detection logic against the real format_connector_error outputs in src/gaia/connectors/formatting.py and it routes correctly for every state; the new test file imports only real symbols and its assertions match the live implementations (grant-migration idempotency, scope constants, _raise_http_for status mapping). No security concerns, no new dependencies.

The single most important thing: the test suite is almost entirely mock-based (it patches check_agent_grant / load_connection / get_provider), so it proves the control flow and HTTP mapping, not that the OAuth scope request is actually accepted by Google/Microsoft. Per CLAUDE.md's "mocks prove we called it, not that the call is valid," the pending live Gmail+Outlook consent capture is the real gate — and you've honestly flagged it as in-progress, which is the right call. Treat that evidence as the merge-blocker, not the unit tests.

Issues Found

🟢 Minor — CTA loses its "reopen sign-in" affordance (EmailConnectCta.tsx:124)

Previously the button stayed clickable after opening OAuth (disabled={busy}, label "Reopen Google sign-in"). Now it's permanently disabled once clicked (disabled={busy || done}, label "… sign-in opened"). If the user accidentally closes the OAuth tab before finishing, the only recovery is re-sending the message — they can't reopen from the CTA. If keeping the reopen path is desired, allowing a re-click restores it (clicking again just re-runs handleConnect):

                disabled={busy}
                aria-label={done ? `Reopen ${label} sign-in` : `Connect ${label}`}

(and the span text would become Reopen ${label} sign-in when done). If the disabled state is intentional — to signal the action already fired — feel free to keep it; flagging the behavior change so it's a deliberate choice.

🟢 Minor — naming drift: resolve_send_backend vs get_send_backend

The test docstrings and PR description refer to resolve_send_backend, but the actual function (and the import in the test) is get_send_backend (hub/agents/python/email/gaia_agent_email/api_routes.py:659). Cosmetic, but it'll trip up a future reader grepping for resolve_send_backend. Worth a find/replace in the docstrings in tests/test_ui_email_scope_consent.py.

🟢 Minor — unused props / belt-and-suspenders branches (no action required)

Two harmless leftovers worth a mental note, not a change:

  • The connectorId prop on EmailConnectCta has no remaining callers — MessageBubble.tsx:512 is the only caller and now passes content. It's tested and safe, just speculative back-compat.
  • The Microsoft branch in isAuthRequiredMessage (connectors → microsoft, etc.) only fires for un-prefixed messages; every real format_connector_error Microsoft output already carries the NOT_CONNECTED: / AGENT_NOT_GRANTED: / AUTH_REQUIRED: prefix caught earlier. It mirrors the Google branch, so keeping it for symmetry is reasonable.

Heads-up (not introduced here) — Outlook user + migration-override message

The installed:email AGENT_NOT_GRANTED override in formatting.py:40 is hardcoded Google-only ("additional Google permissions… Connectors → Google"). An Outlook-only user who somehow hit that specific override would see a Google CTA. In practice Outlook users hit the generic AGENT_NOT_GRANTED (which correctly names microsoft), and that override is keyed to the pre-#962 Google scope-addition migration — so this isn't reachable for Outlook and predates this PR. Noting it only so it's on the radar for the epic's cleanup.

Strengths

  • Detection logic is correct against the source of truth. I checked every branch against format_connector_error: Microsoft NOT_CONNECTED/AGENT_NOT_GRANTED messages carry microsoft (in the prefix text and the Graph scope URIs), so detectProvider routes them to the Microsoft button; ambiguous/AUTH_REQUIRED falls back to showing both. The "never render zero buttons" design is exactly the no-silent-dead-end behavior CLAUDE.md asks for.
  • Tests track real code, not a parallel reality. The grant-migration tests match migrate_legacy_agent_grants's idempotency contract line-for-line, scope assertions read from the live EmailTriageAgent.REQUIRED_CONNECTORS / OUTLOOK_*_SCOPES constants (so they can't silently drift), and the 403/503 mappings assert against the actual _raise_http_for branches.
  • Honest, well-structured PR description — leads with before/after user impact, separates passing unit evidence from pending live evidence, and flags the feat(ui): serve email from the UI backend — mount gaia-agent-email REST router (Path 2) #1774/feat(ui): serve email from the UI backend — mount gaia-agent-email REST router (Path 2) #1768 stacking dependency rather than hiding it.

Verdict

Approve with suggestions. No blocking issues — the change is clean, the refactor (ProviderButton extraction) is sound, and coverage is strong. The 🟢 items are polish. Two non-code caveats for the maintainer: (1) the /v1/email tests depend on the #1768 REST mount, so they'll only go green once #1774 lands — review with/after it as you noted; and (2) the live Gmail+Outlook consent screenshots are the real proof that the negative-state CTAs fire end-to-end, so hold final sign-off on that follow-up.

kovtcharov
kovtcharov previously approved these changes Jun 19, 2026
Base automatically changed from feat/ui-email-router-1768 to main June 19, 2026 19:59
@itomek-amd itomek-amd dismissed kovtcharov’s stale review June 19, 2026 19:59

The base branch was changed.

@itomek-amd itomek-amd enabled auto-merge June 19, 2026 20:00
itomek-amd and others added 2 commits June 19, 2026 16:02
…ct send-backend name in tests

- EmailConnectCta: button stays enabled after opening OAuth (disabled={busy}),
  labelled 'Reopen <provider> sign-in', so a user who closes the OAuth tab can
  reopen it instead of having to re-send the message.
- tests: rename resolve_send_backend -> get_send_backend in docstrings/test name
  to match the real api_routes.get_send_backend symbol.
@itomek

itomek commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks — addressed in 7998aae:

  • CTA reopen affordance (EmailConnectCta.tsx) — fixed. The button is now disabled={busy} (not busy || done) and reads "Reopen <provider> sign-in" once opened, so a user who closes the OAuth tab can reopen it instead of re-sending the message. (20 vitest still green.)
  • resolve_send_backendget_send_backend naming drift — fixed in the test docstrings + method name to match the real api_routes.get_send_backend symbol. (22 py still green.)

For the record on the no-action items:

  • Unused connectorId prop — already tightened in the earlier review pass (narrowed so an unknown value can't render zero buttons); kept as tested back-compat.
  • formatting.py:40 Google-only installed:email override heads-up — confirmed not reachable for Outlook (Outlook hits the generic AGENT_NOT_GRANTED naming microsoft) and predates this PR; flagging it for the epic's cleanup.

Also retargeted this PR to main now that #1774 (the #1768 mount) has merged.

@itomek-amd itomek-amd added this pull request to the merge queue Jun 19, 2026
Merged via the queue into main with commit e9dd58f Jun 19, 2026
45 of 57 checks passed
@itomek-amd itomek-amd deleted the feat/ui-email-scope-consent-1770 branch June 19, 2026 20:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

electron Electron app changes tests Test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Path 2: preserve email connector grant + scope consent in Agent UI (incl. insufficient-scope fail-loud tests)

3 participants